package edu.gmu.naming;

import calendar.CalendarMgr;
import calendar.CalendarMgrSkeleton;
import calendar.Type;
import edu.gmu.client.NamingServiceStub;
import edu.gmu.messages.CreateManagerRequest;
import edu.gmu.messages.CreateManagerResponse;
import edu.gmu.messages.EchoRequest;
import edu.gmu.messages.EchoResponse;
import edu.gmu.messages.ReplicateRequest;
import edu.gmu.messages.EndPoint;
import edu.gmu.messages.EnterRequest;
import edu.gmu.messages.EnterResponse;
import edu.gmu.messages.Neighbors;
import edu.gmu.messages.NeighborsRequest;
import edu.gmu.messages.NeighborsResponse;
import edu.gmu.messages.ExchangeRequest;
import edu.gmu.messages.ExchangeResponse;
import edu.gmu.messages.ReplicateResponse;
import edu.gmu.messages.Resource;
import edu.gmu.messages.ListManagerRequest;
import edu.gmu.messages.ListManagerResponse;
import edu.gmu.messages.Topic;
import edu.gmu.middleware.RemoteStub;
import edu.gmu.middleware.Message;
import edu.gmu.middleware.Sink;
import edu.gmu.middleware.SinkFactory;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;
import java.util.List;

public class NamingService
{
    public static final int EXCHANGE_INTERVAL      = 2000;
    public static final int WELL_KNOWN_NAMING_PORT = 5000;
    public static final int NEIGHBOR_LOOKAHEAD     = 2;
    
    private int id = -1;
    private ServerSocket serverSocket = null;
    private volatile boolean stopped  = true;
    
    // Ring Maintenance State
    private EndPoint localhost  = null;
    private Neighbors neighbors = null;
    
    private int nextCalendarMgrPort = -1;
    
    /**
     * Creates a new NamingService with specified params.
     */
    public NamingService( int id, EndPoint localhost )
    {
        this.id                  = id;
        this.localhost           = localhost;
        this.nextCalendarMgrPort = localhost.port;
        
        /*
         * Note that this neighbor information is primarily for the bootstrap
         * Once we have located the next/prev neighbors, these values
         * will likely be overwritten unless no failures have occurred
         */
        //System.out.println("Our Address: " + ourEndPoint);
    }
    
    public EndPoint getEndPoint()
    {
        return this.localhost;
    }
    
    public int getId()
    {
        return this.id;
    }
    
    public void nobootstrap(List<EndPoint> prevNeighbors,  List<EndPoint> nextNeighbors )
    {
        this.neighbors = new Neighbors( localhost,
                                        nextNeighbors,
                                        prevNeighbors );
    }
    
    /**
     * Perform NamingService Bootstrap procedure.
     * 1. while not in ring (with prev and prevPrev known)
     *        try our "a priori" prev, prevPrev information
     * 2. notify prevNeighbor to link us in as their next
     * 3. set our next to that of prevNeighbor next (will ping later)
     * 4. determine if our calendarMgr is already running on another server
     *    if true
     *        do not try to start, wait to be picked for replica
     *    if false
     *        look for previous data file (do this for other data files???)
     *        if exists
     *            create/start calendarMgr(logicalAddress) from file
     *        else
     *            create/start new calendarMgr(logicalAddress)
     * 5. start handling requests for nextNeighbor and calendarMgr requests
     */
    public void bootstrap( List<EndPoint> prevNeighbors, List<EndPoint> nextNeighbors )
    {
        this.neighbors = new Neighbors( localhost,
                                        nextNeighbors, // default to specified
                                        new ArrayList<EndPoint>() );
        boolean joinedRing = false;
        //while( joinedRing == false && this.stopped == false )
        while( joinedRing == false )
        {
            try
            {
                Thread.sleep(2000);
                System.out.println("Starting - bootstrap");
            }
            catch( InterruptedException ie )
            {
                // ignore
            }
            
            /*
             * Find prev (even if no next?)
             */
            NamingServiceStub prevNeighborStub = null;
            for (int i = 0; i < prevNeighbors.size(); i++)
            {
                EndPoint aNeighbor = prevNeighbors.get(i);
                // Try to connect
                NamingServiceStub aStub = NamingServiceStub.make(aNeighbor);
                if( aStub != null )
                {
                    prevNeighborStub = aStub;
                    break;
                }
            }
            
            // If we have a good connection to both a next and previous
            if( prevNeighborStub != null )
            {
                try
                {
                    // Get neighbor information from our nextNeighbor
                    Neighbors prevNeighborNeighbors = prevNeighborStub.getNeighbors();
                    
                    List<EndPoint> prev = prevNeighborNeighbors.prev;
                    
                    prev = this.validateNeighbors(prev);
                    
                    // If the lists are zero - then we have a small ring
                    // Ignore until more servers come up???
                    //System.out.println("  prev.size(): "+prev.size());
                    if( prev.size() > 0 )
                    {
                        /*
                         * We are close - build our Neighbors Table and 
                         * finally notify prev of our entry into ring!!!
                         * later will ping next so they know who we are
                         */
                        // Told you so - the initial list is for bootstrap only
                        // Now we hope that the list is updated dynamically
                        this.updatePrev(prevNeighborNeighbors);
                        
                        EnterResponse enterResponse = prevNeighborStub.enterRequest(this.neighbors);
                        System.out.println("Got EnterResponse: "+enterResponse.isAccepted());
                        if( enterResponse.isAccepted() )
                        {
                            // Make sure that we were given a valid next list
                            // Otherwise empty next list would propogate
                            // If so - we use specified next provided be caller
                            List<EndPoint> newNext = enterResponse.neighbors.next;
                            newNext = this.validateNeighbors(newNext);
                            if( newNext.size() > 0 )
                            {
                                this.neighbors.next = newNext;
                            }
                            System.out.println("         - bootstrap succeeded");
                            joinedRing = true;
                        }
                    }
                    
                }
                catch(IOException ioe)
                {
                    ioe.printStackTrace();
                    // Problem occurred during linking in - fail try again
                }
            }
        }
        
    }
    
    public void updateNext( Neighbors nextNeighborNeighbors )
    {
        // Get neighbor information from our nextNeighbor
        List<EndPoint> next = nextNeighborNeighbors.next;

        next = this.validateNeighbors(next);
        
        this.neighbors.next.clear();
        this.neighbors.next.add( nextNeighborNeighbors.getNeighbor() );
        this.neighbors.next.addAll( next );
    }
    
    public void updatePrev( Neighbors prevNeighborNeighbors )
    {
        // Get neighbor information from our nextNeighbor
        List<EndPoint> prev = prevNeighborNeighbors.next;

        prev = this.validateNeighbors(prev);

        this.neighbors.prev.clear();
        this.neighbors.prev.add( prevNeighborNeighbors.getNeighbor() );
        this.neighbors.prev.addAll( prev );
    }
    
    /**
     * This method makes sure that we do not reference ourself in the list.
     * If we do, then our entry and any neighbors after our entry are removed.
     * Also note that this limits the neighbors to the lookahead - otherwise
     * the lists would grow to be the size of the ring!!!
     * @param neighbors
     */
    public List<EndPoint> validateNeighbors( List<EndPoint> neighbors )
    {
        List<EndPoint> newNeighbors = new ArrayList<EndPoint>();
        for (int i = 0; i < NEIGHBOR_LOOKAHEAD-1 && i < neighbors.size(); i++)
        {
            EndPoint neighbor = neighbors.get(i);
            if( neighbor.address.equals(this.localhost) )
            //if( neighbor.port == this.localhost.port )
            {
                break;
            }
            newNeighbors.add(neighbor);
        }
        return newNeighbors;
    }
    
    
    public void start() throws IOException
    {
        System.out.println("Listening to - "+this.neighbors.neighbor.port);
        this.serverSocket = new ServerSocket(this.neighbors.neighbor.port);
        
        this.stopped = false;
        /*
         * Handles in separate thread
         */
        Runnable serverTask = new Runnable()
        {
            public void run()
            {   
                while( stopped == false )
                {
                    try
                    {   
                        /*
                         * Handles all request in one thread - so make haste!
                         */
                        processClient( );
                    }
                    catch( Exception ioe )
                    {
                        // ignore??
                    }
                }
            }
        };
        Thread serverThread = new Thread( serverTask );
        serverThread.setName( "NamingServiceThread" );
        serverThread.start();
        
        /*
         * Handles in separate thread
         */
        Runnable exchangeTask = new Runnable()
        {
            public void run()
            {
                while( stopped == false )
                {
                    try
                    {
                        Thread.sleep(EXCHANGE_INTERVAL/2);
                        processExchange();
                    }
                    catch( Exception ie )
                    {
                        // ignore
                    }
                }
            }
        };
        Thread exchangeThread = new Thread( exchangeTask );
        exchangeThread.setName( "ExchangeServiceThread" );
        exchangeThread.start();
    }
    
    
    
    /**
     * Starts exchanging daemon - which also serves as failure-detector.
     */
    public void processExchange( )
    {
        //--------------------------------------------------------------------//
        // Executed in ExchangeServiceThread
        //--------------------------------------------------------------------//
        /*
         * Try our immediate next first
         */
        EndPoint immNext = this.neighbors.getNext().get(0);
        int maxAttempts = 2;
        boolean success = false;
        for( int i = 0; i < maxAttempts; i++ )
        {
            try
            {   
                NamingServiceStub stub = new NamingServiceStub( immNext );
                Neighbors nextNeighbors = stub.exchange(neighbors);

                // All succeeded - update our next neighbor stuff
                this.updateNext(nextNeighbors);
                success = true;
                break;
            }
            catch(IOException ioe)
            {
                // ignore
            }
            try
            {
                Thread.sleep(EXCHANGE_INTERVAL/(maxAttempts*2));
            }
            catch( Exception ie )
            {
                // ignore
            }
        }
        
        if( success == false )
        {
            System.out.println("ExchangeServiceThread - Lost next " + immNext);
            
            /*
             * Failure of our next neighbor has occurred
             * 
             * 1. Connect to next's next
             * 2. Replicate any primary's replicas on failed next to new next
             */
            //this.neighbors.next.remove(0);
            EndPoint newNext = this.neighbors.next.get(1);
            NamingServiceStub stub = new NamingServiceStub(newNext);
            try
            {
                Neighbors nextNeighbors = stub.exchange(neighbors);
                // remove failed next - next ping will update nextNext
                this.neighbors.next.remove(0);
                this.updateNext(nextNeighbors);
                System.out.println("ExchangeServiceThread - Reconnected to " + newNext);
            }
            catch( IOException ioe )
            {
                // ignore
                //ioe.printStackTrace();
            }
        }
        
        //--------------------------------------------------------------------//
    }
    
    /**
     * Handles a new client connection.
     * @param Socket clientSocket
     */
    public void processClient( )
    {
        //--------------------------------------------------------------------//
        // Executed in handlerThread
        //--------------------------------------------------------------------//
        RemoteStub remoteStub = null;
        try
        {
            // Blocks until client connects
            Socket clientSocket = this.serverSocket.accept();
            Sink sink  = SinkFactory.makeTCPSink(clientSocket );
            remoteStub = new RemoteStub( sink, 999, "NamingServiceStub" );
            
            remoteStub.register(Topic.CREATE_MANAGER_REQUEST, CreateManagerRequest.class);
            remoteStub.register(Topic.REPLICATE_REQUEST,      ReplicateRequest.class);
            remoteStub.register(Topic.LIST_MANAGER_REQUEST,   ListManagerRequest.class);
            remoteStub.register(Topic.NEIGHBORS_REQUEST,      NeighborsRequest.class);
            remoteStub.register(Topic.ENTER_REQUEST,          EnterRequest.class);
            remoteStub.register(Topic.EXCHANGE_REQUEST,       ExchangeRequest.class);
            remoteStub.register(Topic.ECHO_REQUEST,           EchoRequest.class);
            // This is adding to shared list from this thread - need to protect
            
            Message message = remoteStub.receive();
            if( message instanceof CreateManagerRequest )
            {
                CreateManagerRequest request = (CreateManagerRequest)message;
                
                CalendarMgrSkeleton mgrSkel = this.addCalendarMgr(request.managerId, Type.PRIMARY, null);
                
                // Ugly - but give a little time for CalendarMgr to start
                try{Thread.sleep(250);}catch(Exception ioe){}
                
                CreateManagerResponse response = new CreateManagerResponse();
                response.calendarMgrEndpoint = mgrSkel.getEndPoint();
                
                remoteStub.send(Topic.CREATE_MANAGER_RESPONSE, response);
            }
            else if( message instanceof ListManagerRequest )
            {
                ListManagerRequest request = (ListManagerRequest)message;
                
                ListManagerResponse response = new ListManagerResponse();
                response.namingServiceId = this.id;
                response.localhost       = this.localhost;
                response.request         = request;
                List<Resource> resources = new ArrayList<Resource>();
                for (int i = 0; i < this.calendarMgrs.size(); i++)
                {
                    CalendarMgrSkeleton skel = calendarMgrs.get(i);
                    CalendarMgr calMgr = skel.getCalendarMgr();
                    
                    Resource resource = new Resource();
                    resource.id       = calMgr.getId();
                    resource.endpoint = skel.getEndPoint();
                    resource.type     = calMgr.getType();
                    resources.add(resource);
                }
                response.resources = resources;
                
                // Need to check these first
                if( this.neighbors.next.size() > 0 )
                {
                    response.next = this.neighbors.next.get(0);
                }
                if( this.neighbors.prev.size() > 0 )
                {
                    response.prev = this.neighbors.prev.get(0);
                }

                remoteStub.send(Topic.LIST_MANAGER_RESPONSE, response);
            }
            else if( message instanceof ReplicateRequest )
            {
                ReplicateRequest request = (ReplicateRequest)message;
                
                EndPoint primary = request.primary;
                int replicaId    = request.id;
                byte[] fileBytes = request.file;
                
                /*
                 * Sanity check - primary may request replicate twice (or more)
                 * 1. primary call replicate-A
                 * 2.                             create calendarMgr-A
                 * 3. primary tries connect (fails)
                 * 4.                             calendarMgr starts and creates calendars
                 * 5.                             calendars start
                 * ...
                 * 6. primary call replicate-A    create calendarMgr-A !!!!
                 * 
                 * One solution is to delay return or check to prevent
                 * duplicate - the first works, however, went with
                 * checking to be more robust (i.e. less reliant on timing).
                 */
                
                String fileName = CalendarMgr.getFileName(replicaId, Type.BACKUP);
                // I believe this overwrites previous?
                FileOutputStream fos = new FileOutputStream(fileName, false);
                fos.write(fileBytes);
                fos.close();
                
                CalendarMgrSkeleton mgrSkel = this.addCalendarMgr(replicaId, Type.BACKUP, primary);
                
                ReplicateResponse response = new ReplicateResponse();
                response.calendarMgrEndpoint = mgrSkel.getEndPoint();
                
                remoteStub.send(Topic.REPLICATE_RESPONSE, response);
            }
            else if( message instanceof NeighborsRequest )
            {
                NeighborsRequest request = (NeighborsRequest)message;
                
                NeighborsResponse response = new NeighborsResponse();
                response.neighbors = this.neighbors;

                remoteStub.send(Topic.NEIGHBORS_RESPONSE, response);
            }
            else if( message instanceof EnterRequest )
            {
                EnterRequest request = (EnterRequest)message;
                
                /*
                 * If we accept - this server becomes our next
                 * Change before or after response? Doesn't matter
                 * the caller is interested in our previous neighbors
                 */
                //System.out.println( "Changing next to " + request.neighbors.getNeighbor() );
                // Do not accept requests from your self - bad stuff
                boolean accept = (request.getNeighbors().getNeighbor().equals(this.localhost) == false);
                EnterResponse response = new EnterResponse();
                response.accepted = accept;
                response.neighbors = this.neighbors;

                // Send our prior neigbor information
                remoteStub.send(Topic.ENTER_RESPONSE, response);
                
                if( accept == true )
                {
                    EndPoint nextNext = this.neighbors.getNext().get(0);
                    this.neighbors.getNext().clear();
                    this.neighbors.getNext().add( request.neighbors.getNeighbor() );
                    this.neighbors.getNext().add( nextNext );

                    System.out.println( "Changing next to " + this.neighbors.next.get(0) );
                    //System.out.println( "Changing next to " + this.neighbors.next.get(0) + " from " +
                    //        request.neighbors.getNeighbor() + " list " + request.neighbors.getNext() );
                }
            }
            else if( message instanceof ExchangeRequest )
            {
                ExchangeRequest request = (ExchangeRequest)message;
                //System.out.println("    Ping from: "+request.neighbors.neighbor);
                
                /*
                 * Our previous is just checking in - update prev info
                 */
                this.updatePrev(request.neighbors);
                
                ExchangeResponse response = new ExchangeResponse();
                response.neighbors = this.neighbors;

                remoteStub.send(Topic.EXCHANGE_RESPONSE, response);
            }
            else if( message instanceof EchoRequest )
            {
                EchoRequest request = (EchoRequest)message;
                
                EchoResponse response = new EchoResponse();
                response.request = request;
                response.num     = request.num+1;

                remoteStub.send(Topic.ECHO_RESPONSE, response);
            }
            else
            {
                System.out.println("Unknown message received "+message);
            }
        }
        catch(IOException ioe)
        {
            ioe.printStackTrace();
        }
        finally
        {
            // Closes stub, sink, socket
            if( remoteStub!= null ) remoteStub.stop();
        }
        //--------------------------------------------------------------------//
    }

    public EndPoint getNextNamingService()
    {
        return this.neighbors.next.get(0);
    }
    
    public CalendarMgrSkeleton getCalendar( int id, Type type )
    {
        CalendarMgrSkeleton calendar = null;
        for (int i = 0; i < this.calendarMgrs.size(); i++)
        {
            CalendarMgrSkeleton calendarMgrSkeleton = this.calendarMgrs.get(i);
            CalendarMgr calendarMgr = calendarMgrSkeleton.getCalendarMgr();
            if( calendarMgr.getId() == id &&
                calendarMgr.getType() == type )
            {
                calendar = calendarMgrSkeleton;
                break;
            }
        }
        return calendar;
    }
    
    
//    //------------------------------------------------------------------------//
//    // Persistence Code
//    //------------------------------------------------------------------------//
//    /**
//     * Saves the state of this object to file.
//     * @throws java.io.IOException
//     */
//    public void write( ArrayList calendars ) throws IOException
//    {
//        ObjectOutputStream e = new ObjectOutputStream(
//        //XMLEncoder e = new XMLEncoder(
//                          new BufferedOutputStream(
//                              new FileOutputStream("Calendars.ser")));
//        e.writeObject(new Integer(this.nextEventId) );
//        e.writeObject(this.calendars);
//        e.close();
//    }
//    
//    /**
//     * Restores the state of this object from file.
//     * @throws java.io.IOException
//     */
//    public void read() throws IOException, ClassNotFoundException
//    {
//        ObjectInputStream d = new ObjectInputStream(
//        //XMLDecoder d = new XMLDecoder(
//                          new BufferedInputStream(
//                              new FileInputStream("Calendars.ser")));
//        Object oNextEventId = d.readObject();
//        this.nextEventId = ((Integer)oNextEventId).intValue();
//        Object result = d.readObject();
//        this.calendars = (List <Calendar>)result;
//        // Set all calendars to closed
//        for (int i = 0; i < calendars.size(); i++)
//        {
//            Calendar calendar = calendars.get(i);
//            calendar.opened = false;
//        }
//        
//        d.close();
//    }
    
    /**
     * Stops the server from listening
     * @throws java.io.IOException
     */
    public void stop()
    {
        this.stopped = true;
        // Stop server - so that we accept no more clients
        if( this.serverSocket != null )
        {
            try
            {
                this.serverSocket.close();
            }
            catch(IOException ioe)
            {
                // ignore
            }
        }
    }
    
    
    //------------------------------------------------------------------------//
    // CalendarManager code
    //------------------------------------------------------------------------//
    private List<CalendarMgrSkeleton> calendarMgrs = new ArrayList<CalendarMgrSkeleton>(); 
    
    public CalendarMgrSkeleton addCalendarMgr( int id, Type type, EndPoint primary )
    {
        /*
         * Do not create twice - only allow one instance of managerId+type
         */
        CalendarMgrSkeleton prevStub = this.getCalendar(id, type);
        if( prevStub != null )
        {
            System.out.println("Attempt to create duplicate " + id + " " + type);
            return prevStub;
        }
        
        this.nextCalendarMgrPort+=100;
        EndPoint endpoint = new EndPoint( this.localhost.address, nextCalendarMgrPort );
        CalendarMgr calendarMgr = new CalendarMgr( id, endpoint, type, this, primary );
        //System.out.println("Starting manager-" + managerId + "-"+type + " on " + nextCalendarMgrPort);
        CalendarMgrSkeleton stub = new CalendarMgrSkeleton( calendarMgr,
                                                            endpoint,
                                                            "CalendarManager-"+id+ "-"+ type );
        this.calendarMgrs.add(stub);
        
        return stub;
    }
    
    
    //------------------------------------------------------------------------//
    // Startup code
    //------------------------------------------------------------------------//
    /**
     * Starts the naming serice on the localhost+well-known-port
     * @param args
     * @throws java.io.IOException
     */
    public static void main( String[] args ) throws IOException
    {
        String usage = "Usage: NamingServer pp p l n nn <bootstrap>" +
                         "\n       e.g. />NamingServer 4 5 1 2 3 bootstrap";
        if( args.length != 5 && args.length != 6 )
        {   
            System.out.println(usage);
            return;
        }
        
        int prevPrev = Integer.parseInt(args[0]);
        int prev     = Integer.parseInt(args[1]);
        int id       = Integer.parseInt(args[2]);
        int next     = Integer.parseInt(args[3]);
        int nextNext = Integer.parseInt(args[4]);
        
        boolean bootstrap = false;
        if( args.length == 6 )
        {
            bootstrap = Boolean.parseBoolean(args[5]);
        }
        
        EndPoint localhost = EndPoint.deriveHostAddress(id);
        
        List<EndPoint> nextNeighbors = new ArrayList<EndPoint>();
        List<EndPoint> prevNeighbors = new ArrayList<EndPoint>();
        
        nextNeighbors.add( EndPoint.deriveHostAddress(next) );
        nextNeighbors.add( EndPoint.deriveHostAddress(nextNext) );
        
        prevNeighbors.add( EndPoint.deriveHostAddress(prev) );
        prevNeighbors.add( EndPoint.deriveHostAddress(prevPrev) );
        
        NamingService server = new NamingService( id, localhost );
        System.out.println("Bootstrap: " + bootstrap);
        
        if( bootstrap == false )
        {   
            /*
             * Sleep a bit before actually starting to give others time to start
             */
            try
            {
                Thread.sleep(5000);
                server.nobootstrap( prevNeighbors, nextNeighbors );
            }
            catch( InterruptedException ie )
            {
                // ignore
            }
            
            server.start();
            
            // Check to see if a primary file exists that we'll start with
            File file = new File( CalendarMgr.getFileName(id, Type.PRIMARY) );
            if( file.exists() == true )
            {
                server.addCalendarMgr(id, Type.PRIMARY, null);
            }
        }
        else
        {
            /*
             * This may take some time but starts trying immediately
             */
            server.bootstrap( prevNeighbors, nextNeighbors );
            
            server.start();
        }
        
    }
    

}
